import { ILogger, LogLevel } from '@theia/core/lib/common/logger';

export interface DaemonLog {
  readonly time: string;
  readonly level: DaemonLog.Level;
  readonly msg: string;
}

export namespace DaemonLog {
  export interface Url {
    readonly Scheme: string;
    readonly Host: string;
    readonly Path: string;
  }

  export namespace Url {
    export function is(arg: any | undefined): arg is Url {
      return (
        !!arg &&
        typeof arg.Scheme === 'string' &&
        typeof arg.Host === 'string' &&
        typeof arg.Path === 'string'
      );
    }

    export function toString(url: Url): string {
      const { Scheme, Host, Path } = url;
      return `${Scheme}://${Host}${Path}`;
    }
  }

  export interface System {
    readonly os: string;
    // readonly Resource: Resource;
  }

  export namespace System {
    export function toString(system: System): string {
      return `OS: ${system.os}`;
    }
  }

  export interface Tool {
    readonly version: string;
    readonly systems: System[];
  }

  export namespace Tool {
    export function is(arg: any | undefined): arg is Tool {
      return !!arg && typeof arg.version === 'string' && 'systems' in arg;
    }

    export function toString(tool: Tool): string {
      const { version, systems } = tool;
      return `Version: ${version}${
        !!systems
          ? ` Systems: [${tool.systems.map(System.toString).join(', ')}]`
          : ''
      }`;
    }
  }

  export type Level = 'trace' | 'debug' | 'info' | 'warning' | 'error';

  export function is(arg: any | undefined): arg is DaemonLog {
    return (
      !!arg &&
      typeof arg.time === 'string' &&
      typeof arg.level === 'string' &&
      typeof arg.msg === 'string'
    );
  }

  export function toLogLevel(log: DaemonLog): LogLevel {
    const { level } = log;
    switch (level) {
      case 'trace':
        return LogLevel.TRACE;
      case 'debug':
        return LogLevel.DEBUG;
      case 'info':
        return LogLevel.INFO;
      case 'warning':
        return LogLevel.WARN;
      case 'error':
        return LogLevel.ERROR;
      default:
        return LogLevel.INFO;
    }
  }

  export function log(logger: ILogger, logMessages: string): void {
    const parsed = parse(logMessages);
    for (const log of parsed) {
      const logLevel = toLogLevel(log);
      const message = toMessage(log, { omitLogLevel: true });
      logger.log(logLevel, message);
    }
  }

  function parse(toLog: string): DaemonLog[] {
    const messages = toLog.trim().split('\n');
    const result: DaemonLog[] = [];
    for (let i = 0; i < messages.length; i++) {
      try {
        const maybeDaemonLog = JSON.parse(messages[i]);
        if (DaemonLog.is(maybeDaemonLog)) {
          result.push(maybeDaemonLog);
          continue;
        }
      } catch {
        /* NOOP */
      }
      result.push({
        time: new Date().toString(),
        level: 'info',
        msg: messages[i],
      });
    }
    return result;
  }

  export function toPrettyString(logMessages: string): string {
    const parsed = parse(logMessages);
    return parsed.map((log) => toMessage(log)).join('\n') + '\n';
  }

  function toMessage(
    log: DaemonLog,
    options: { omitLogLevel: boolean } = { omitLogLevel: false }
  ): string {
    const details = Object.keys(log)
      .filter((key) => key !== 'msg' && key !== 'level' && key !== 'time')
      .map((key) => toDetails(log, key))
      .join(', ');
    const logLevel = options.omitLogLevel
      ? ''
      : `[${log.level.toUpperCase()}] `;
    return `${logLevel}${log.msg}${!!details ? ` [${details}]` : ''}`;
  }

  function toDetails(log: DaemonLog, key: string): string {
    let value = (log as any)[key];
    if (DaemonLog.Url.is(value)) {
      value = DaemonLog.Url.toString(value);
    } else if (DaemonLog.Tool.is(value)) {
      value = DaemonLog.Tool.toString(value);
    } else if (typeof value === 'object') {
      value = JSON.stringify(value).replace(/\"([^(\")"]+)\":/g, '$1:'); // Remove the quotes from the property keys.
    }
    return `${key.toLowerCase()}: ${value}`;
  }
}
